package com.magenta.geocoding.cloudmade;

import com.magenta.geocoding.cloudmade.json.*;
import com.magenta.maxoptra.dto.GeoLocationRecord;
import com.magenta.maxoptra.gis.*;
import com.magenta.maxoptra.gis.utils.*;
import com.magenta.mrs.basegeocoding.BaseGeocoding;
import com.magenta.mrs.util.GeoInformationClient;
import org.apache.http.HttpHost;
import org.apache.http.HttpResponse;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.Credentials;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.conn.params.ConnRoutePNames;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.util.EntityUtils;
import org.codehaus.jackson.map.ObjectMapper;

import java.io.IOException;
import java.net.Authenticator;
import java.net.InetSocketAddress;
import java.net.PasswordAuthentication;
import java.net.SocketAddress;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;

import static com.magenta.maxoptra.gis.GisParameter.*;

public class GeoProviderCloudmade extends BaseGeocoding<String> implements IGeocoding, IReverseGeocoding, IGeoRouting {

    protected java.net.Proxy proxy;

    public String getProviderName() {
        return MapProviderType.cloudmade.name();
    }

    @Override
    protected GisParameters getProviderParams() {
        return client.getProperties();
    }

    @Override
    protected boolean accuracyIsAccurateEnough(String accuracyThreshold, String accuracy) {
        return false;
    }

    @Override
    protected List<GeoAddress> doFindAddressByKeyword(GeoProviderSettings settings, String keyword, int resultAddressLimit, List<String> accuracies) throws GeoException {
        return findAddressByKeyword(settings, keyword, resultAddressLimit);
    }

    @Override
    public void setClient(GeoInformationClient client) {
        this.client = client;
        HttpHost httpProxy = (HttpHost) client.getParams().getParameter(ConnRoutePNames.DEFAULT_PROXY);
        if (httpProxy != null) {
            String proxyHost = httpProxy.getHostName();
            int proxyPort = httpProxy.getPort();
            final Credentials credentials = ((DefaultHttpClient) client).getCredentialsProvider().getCredentials(
                    new AuthScope(proxyHost, proxyPort));

            SocketAddress socketAddress = new InetSocketAddress(proxyHost, proxyPort);
            this.proxy = new java.net.Proxy(java.net.Proxy.Type.HTTP, socketAddress);

            Authenticator.setDefault(new Authenticator() {
                protected PasswordAuthentication getPasswordAuthentication() {
                    PasswordAuthentication p = new PasswordAuthentication(credentials.getUserPrincipal().getName(),
                            credentials.getPassword().toCharArray());
                    return p;
                }
            });
        }

    }

    private String doRequest(String url) throws GeoException {
        try {
            String encodedUrl = org.apache.commons.httpclient.util.URIUtil.encodeQuery(url);
            HttpGet get = new HttpGet(encodedUrl);
            get.addHeader("Accept-Language", "ru-ru,ru;q=0.8,en-us;q=0.5,en;q=0.3");

            HttpResponse response = client.execute(get);
            return EntityUtils.toString(response.getEntity(), "utf-8");
        } catch (Exception e) {
            throw new GeoException(e);
        }
    }

    private String doForwardGeocodingRequest(String keyWord, int resultAddressLimit) throws GeoException {
        String forwardGeocodingURL = getForwardGeocodingURL();
        String mapKey = getMapKey();
        //http://geocoding.cloudmade.com/YOUR-API-KEY/geocoding/v2/find.output_type?query=SEARCH PHRASE&other_parameters
        if (forwardGeocodingURL != null && mapKey != null) {
            forwardGeocodingURL = String.format(forwardGeocodingURL, mapKey);
            forwardGeocodingURL = forwardGeocodingURL + "query=" + formatString(keyWord) + "&results=" + resultAddressLimit;
        }
        return doRequest(forwardGeocodingURL);
    }

    public List<GeoAddress> findAddressByKeyword(GeoProviderSettings settings, String keyword, int resultAddressLimit) throws GeoException {
        ArrayList<GeoAddress> shortAddresses = new ArrayList<GeoAddress>();
        try {
            String addresses = doForwardGeocodingRequest(keyword, resultAddressLimit);
            ObjectMapper mapper = new ObjectMapper();
            SearchResponse response = mapper.readValue(addresses, SearchResponse.class);
//            if (response.getSuccess()) {
            List<Feature> responses = response.getFeatures();
            if (responses != null) {
                for (Feature feature : responses) {
                    CMProperties properties = feature.getProperties();
                    Centroid centroid = feature.getCentroid();
                    if (centroid == null) continue;
                    GeoAddress address = new GeoAddress();
                    address.setInfo(properties.getName());
                    address.setLatitude(centroid.getCoordinates()[0]);
                    address.setLongitude(centroid.getCoordinates()[1]);
                    address.setHouse(properties.getHousenumber());
                    address.setStreet(properties.getStreet());
                    address.setZip(properties.getPostcode());
                    shortAddresses.add(address);
                }
            }
        } catch (Exception e) {
            throw new GeoException(e);
        }
        return shortAddresses;
    }

    @Override
    public List<GeoAddress> doForwardGeocoding(GeoProviderSettings settings, GeoAddress address, int resultAddressLimit)
            throws GeoException {
        DoForwardGeocodingProfiler profiler = new DoForwardGeocodingProfiler(getProviderName(), getAccountGlobalId(), isLoggingAllowed(), address);
        try {
            List<GeoAddress> addressByKeyword = findAddressByKeyword(settings, address.getInfo(), resultAddressLimit);
            profiler.logDuration(addressByKeyword.size());
            return addressByKeyword;
        } catch (GeoException e) {
            profiler.logError(e.getMessage());
            throw e;
        }
    }

    public GeoAddress findBestAddressByKeyword(GeoProviderSettings settings, String keyword) throws GeoException {
        DoForwardGeocodingProfiler profiler = new DoForwardGeocodingProfiler(getProviderName(), getAccountGlobalId(), isLoggingAllowed(), keyword);
        try {
            List<GeoAddress> addressByKeyword = findAddressByKeyword(settings, keyword, 1);
            if (addressByKeyword != null && addressByKeyword.size() > 0) {
                GeoAddress geoAddress = addressByKeyword.get(0);
                profiler.logDuration(geoAddress);
                return geoAddress;
            } else {
                profiler.logError("Empty result");
                return null;
            }
        } catch (GeoException e) {
            profiler.logError(e.getMessage());
            throw e;
        }
    }


    private String doReverseGeocodingRequest(GeographicCoordinates geoCoordinates) throws GeoException {
        String reverseGeocodingURL = getReverseGeocodingURL();
        String mapKey = getMapKey();
        //http://geocoding.cloudmade.com/YOUR-API-KEY/geocoding/v2/find.html?around=[object,point]&object_type=OBJECT_TYPE
        if (reverseGeocodingURL != null && mapKey != null) {
            reverseGeocodingURL = String.format(reverseGeocodingURL, mapKey);
            reverseGeocodingURL = reverseGeocodingURL + "object_type=address&around=" + formatDouble(geoCoordinates.getLatitude()) + "," + formatDouble(geoCoordinates.getLongitude()) + "&results=1";
        }
        return doRequest(reverseGeocodingURL);
    }


    @Override
    public GeoAddress doReverseGeocoding(GeographicCoordinates geoCoordinates) throws GeoException {
        String addresses = "";
        DoReverseGeocodingProfiler profiler = new DoReverseGeocodingProfiler(getProviderName(), getAccountGlobalId(), isLoggingAllowed(), geoCoordinates);
        try {
            addresses = doReverseGeocodingRequest(geoCoordinates);
            ObjectMapper mapper = new ObjectMapper();
            SearchResponse response = mapper.readValue(addresses, SearchResponse.class);
            if (response != null) {
                GeoAddress address = new GeoAddress();
                List<Feature> responses = response.getFeatures();
                if (responses != null) {
                    for (Feature feature : responses) {
                        CMProperties properties = feature.getProperties();
                        Centroid centroid = feature.getCentroid();
                        if (centroid == null) continue;
                        address.setInfo(properties.getName());
                        address.setLatitude(centroid.getCoordinates()[0]);
                        address.setLongitude(centroid.getCoordinates()[1]);
                        address.setHouse(properties.getHousenumber());
                        address.setStreet(properties.getStreet());
                        address.setZip(properties.getPostcode());
                    }
                    profiler.logDuration(address);
                    return address;
                }
            }
        } catch (IOException e) {
            profiler.logError(e.getMessage());
            if ("Wrong api key".equalsIgnoreCase(addresses)) {
                throw new GeoException(addresses);
            } else {
                throw new GeoException(e);
            }
        }
        profiler.logError("Empty result");
        return null;
    }

    @Override
    public List<GeoAddress> doReverseGeocodeFull(List<GeographicCoordinates> geoCoordinates) throws GeoException {
        List<GeoAddress> result = new ArrayList<GeoAddress>();
        for (GeographicCoordinates geoCoordinate : geoCoordinates) {
            result.add(doReverseGeocoding(geoCoordinate));
        }
        return result;
    }

    private String getRoutingURL() {
        return getGisStringProperty(routing_url);
    }

    private String getForwardGeocodingURL() {
        return getGisStringProperty(geocoding_url);
    }

    private String getReverseGeocodingURL() {
        return getGisStringProperty(reverse_geocoding_url);
    }

    private String getMapKey() {
        return getGisStringProperty(map_key);
    }

    private boolean useShortestRoute() {
        return getGisBooleanProperty(use_shortest_route);
    }

    private static String formatDouble(Double value) {
        DecimalFormat decimalFormat = new DecimalFormat("0.00000");
        DecimalFormatSymbols newSymbols = new DecimalFormatSymbols();
        newSymbols.setDecimalSeparator('.');
        decimalFormat.setDecimalFormatSymbols(newSymbols);
        return decimalFormat.format(value);
    }


    private String doRoutingRequest(List<GeographicCoordinates> coords) throws GeoException {
        String routingURL = getRoutingURL();
        String mapKey = getMapKey();
        boolean useShortestRoute = useShortestRoute();
        //http://routes.cloudmade.com/YOUR-API-KEY-GOES-HERE/api/0.3/
        // start_point,[transit_point1,...,transit_pointN],
        // end_point/route_type[/route_type_modifier].output_format[?lang=(Two letter ISO 3166-1 code)][&units=(km|miles)]
        if (routingURL != null && mapKey != null) {
            routingURL = routingURL + mapKey + "/api/0.3/" + routingPointsString(coords) + "/car/" + (useShortestRoute ? "shortest" : "fastest") + ".js";
        }
        return doRequest(routingURL);
    }

    private GeoRoute findRoute(List<GeographicCoordinates> coords) throws GeoException {
        GetRouteByListProfiler profiler = new GetRouteByListProfiler(getProviderName(), getAccountGlobalId(), isLoggingAllowed(), coords);
        try {
            String response = doRoutingRequest(coords);
            CloudmadeRoute cloudmadeRoute = new ObjectMapper().readValue(response, CloudmadeRoute.class);
            RouteSummary route_summary = cloudmadeRoute.getRoute_summary();
            if (cloudmadeRoute.getStatus() == 0 && route_summary != null) {
                GeoRoute geoRoute = new GeoRoute(UnitsConverter.convert(LineUnits.METER, LineUnits.KILOMETRE, route_summary.getTotal_distance()),
                        route_summary.getTotal_time() * DateTimeHelper.MSECS_IN_SECOND, coords.get(0), coords.get(coords.size() - 1));
                profiler.logDuration(geoRoute);
                return geoRoute;
            } else {
                String message = "Provider error " + cloudmadeRoute.getStatus() + ": " + cloudmadeRoute.getStatus_message();
                profiler.logError(message);
                throw new GeoException(message);
            }
        } catch (IOException e) {
            profiler.logError(e.getMessage());
            throw new GeoException(e);
        }
    }

    @Override
    public GeoRoute getRoute(GeographicCoordinates from, GeographicCoordinates to, VehicleType vehicleType) throws GeoException {
        List<GeographicCoordinates> coords = new LinkedList<GeographicCoordinates>();
        coords.add(from);
        coords.add(to);
        return findRoute(coords);
    }

    @Override
    public GeoRoute getRoute(List<GeographicCoordinates> coordinates, VehicleType vehicleType) throws GeoException {
        if (coordinates != null && coordinates.size() > 1) {
            return getRoute(coordinates);
        }
        return null;
    }

    @Override
    public List<GeoRoute> getRoute(GeographicCoordinates from, List<? extends GeographicCoordinates> to, VehicleType vehicleType, int timeout) throws GeoException {
        throw new UnsupportedOperationException("getRoute from coordinate to list of coordinates is not supported for " + getProviderName());
    }

    private String routingPointsString(List<GeographicCoordinates> points) {

//http://routes.cloudmade.com/8ee2a50541944fb9bcedded5165f09d9/api/0.3/
//    51.22545,4.40730,[51.22,4.41,51.2,4.41],51.23,4.42
// /car.js?lang=de&units=miles
        String res = "";
        int size = points.size();
        if (size > 1) {
            res = res + formatDouble(points.get(0).getLatitude()) + "," + formatDouble(points.get(0).getLongitude()) + ",";
            if (size == 2) {
                res = res + formatDouble(points.get(1).getLatitude()) + "," + formatDouble(points.get(1).getLongitude());
            } else {
                res = res + "[";
                int n = points.size() - 1;
                for (int i = 1; i < n; i++) {
                    GeographicCoordinates point = points.get(i);
                    res = res + formatDouble(point.getLatitude()) + "," + formatDouble(point.getLongitude()) + (i == n ? "" : ",");
                }
                res = res + "],";
                res = res + formatDouble(points.get(n).getLatitude()) + "," + formatDouble(points.get(n).getLongitude());
            }
        }
        return res;
    }

    private List<GeoLocationRecord> findRoutePath(List<GeographicCoordinates> coords) throws GeoException {
        GetRoutePointsByCoordsProfiler profiler = new GetRoutePointsByCoordsProfiler(getProviderName(), getAccountGlobalId(), isLoggingAllowed(), coords);
        List<GeoLocationRecord> path = new ArrayList<GeoLocationRecord>();
        try {
            String response = doRoutingRequest(coords);
            CloudmadeRoute cloudmadeRoute = new ObjectMapper().readValue(response, CloudmadeRoute.class);
            List<Double[]> route_geometry = cloudmadeRoute.getRoute_geometry();
            if (cloudmadeRoute.getStatus() == 0 && route_geometry != null) {
                for (Double[] point : route_geometry) {
                    path.add(new GeoLocationRecord(point[0], point[1]));
                }
                profiler.logDuration(path.size());
                return path;
            } else {
                String message = "Provider error " + cloudmadeRoute.getStatus() + ": " + cloudmadeRoute.getStatus_message();
                profiler.logError(message);
                throw new GeoException(message);
            }
        } catch (IOException e) {
            profiler.logError(e.getMessage());
            throw new GeoException(e);
        }
    }


    @Override
    public List<GeoLocationRecord> getRoutePoints(GeoProviderSettings settings, GeographicCoordinates from, GeographicCoordinates to) throws GeoException {
        List<GeographicCoordinates> coords = new LinkedList<GeographicCoordinates>();
        coords.add(from);
        coords.add(to);
        return findRoutePath(coords);
    }

    @Override
    public List<GeoLocationRecord> getRoutePoints(GeoProviderSettings settings, List<GeographicCoordinates> points) throws GeoException {
        return findRoutePath(points);
    }

    @Override
    public List<GeoRoute> getRoutes(GeographicCoordinates from, List<? extends GeographicCoordinates> to, VehicleType vehicleType, int timeout) throws GeoException {
        throw new UnsupportedOperationException("getRoutes from coordinate to list of coordinates is not supported for "+getProviderName());
    }

    @Override
    public List<GeoRoute> getCoordinateToCoordinateRoutes(List<GeographicCoordinates> coordinates) throws ServerUnavailableException {
        throw new UnsupportedOperationException("getCoordinateToCoordinateRoutes from coordinate to list of coordinates is not supported for " + getProviderName());
    }

    @Override
    public List<GeoRoute> getRoutes(GeographicCoordinates from, List<? extends GeographicCoordinates> to, int timeout) throws GeoException, ServerUnavailableException {
        return getRoutes(from, to, timeout);
    }

    @Override
    public List<GeoRoute> getRoute(GeographicCoordinates from, List<? extends GeographicCoordinates> to, int timeout) throws GeoException {
        return getRoute(from, to, null, timeout);
    }

    @Override
    public GeoRoute getRoute(List<GeographicCoordinates> coordinates) throws GeoException {
        return getRoute(coordinates, null);
    }

    @Override
    public synchronized GeoRoute getRoute(GeographicCoordinates from, GeographicCoordinates to) throws GeoException, ServerUnavailableException {
        return getRoute(from, to, null);
    }


}

